package net.bull.javamelody;

import static net.bull.javamelody.HttpParameters.COLLECTOR_PARAMETER;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Locale;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * Filtre de servlet pour le monitoring.
 * @author Emeric Vernat
 **/
public class MonitoringFilter implements Filter
{

	private static boolean instanceCreated;
	private final boolean instanceEnabled;

	private Counter httpCounter;
	private Counter errorCounter;

	private boolean monitoringDisabled;
	private boolean logEnabled;
	private Pattern urlExcludePattern;
	private Pattern allowedAddrPattern;
	private FilterContext filterContext;
	private FilterConfig filterConfig;
	private String monitoringUrl;

	public MonitoringFilter()
	{
		super();
		if(instanceCreated)
		{
			instanceEnabled = false;
		}
		else
		{
			instanceEnabled = true;
			setInstanceCreated(true);
		}
	}

	private static void setInstanceCreated(boolean newInstanceCreated)
	{
		instanceCreated = newInstanceCreated;
	}

	public void init(FilterConfig config) throws ServletException
	{
		if(!instanceEnabled)
		{
			return;
		}
		this.filterConfig = config;
		Parameters.initialize(config);
		monitoringDisabled = Boolean.parseBoolean(Parameters.getParameter(Parameter.DISABLED));
		if(monitoringDisabled)
		{
			return;
		}

		LOG.debug("JavaMelody filter init started");

		this.filterContext = new FilterContext();
		final Collector collector = filterContext.getCollector();
		this.httpCounter = collector.getCounterByName(Counter.HTTP_COUNTER_NAME);
		this.errorCounter = collector.getCounterByName(Counter.ERROR_COUNTER_NAME);

		logEnabled = Boolean.parseBoolean(Parameters.getParameter(Parameter.LOG));
		if(Parameters.getParameter(Parameter.URL_EXCLUDE_PATTERN) != null)
		{
			urlExcludePattern = Pattern.compile(Parameters.getParameter(Parameter.URL_EXCLUDE_PATTERN));
		}
		if(Parameters.getParameter(Parameter.ALLOWED_ADDR_PATTERN) != null)
		{
			allowedAddrPattern = Pattern.compile(Parameters.getParameter(Parameter.ALLOWED_ADDR_PATTERN));
		}

		LOG.debug("JavaMelody filter init done");
	}

	public void destroy()
	{
		if(monitoringDisabled || !instanceEnabled)
		{
			return;
		}
		try
		{
			if(filterContext != null)
			{
				filterContext.destroy();
			}
		}
		finally
		{
			httpCounter = null;
			errorCounter = null;
			urlExcludePattern = null;
			allowedAddrPattern = null;
			filterConfig = null;
			filterContext = null;
		}
	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
	{
		if(!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse) || monitoringDisabled || !instanceEnabled)
		{
			chain.doFilter(request, response);
			return;
		}
		final HttpServletRequest httpRequest = (HttpServletRequest) request;
		final HttpServletResponse httpResponse = (HttpServletResponse) response;

		if(httpRequest.getRequestURI().equals(getMonitoringUrl(httpRequest)))
		{
			doMonitoring(httpRequest, httpResponse);
			return;
		}
		if(!httpCounter.isDisplayed() || isRequestExcluded((HttpServletRequest) request))
		{
			chain.doFilter(request, response);
			return;
		}

		doFilter(chain, httpRequest, httpResponse);
	}

	private void doFilter(FilterChain chain, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
	{
		HttpServletRequest wrappedRequest = JspWrapper.createHttpRequestWrapper(httpRequest);
		if(httpRequest.getContentType() != null && httpRequest.getContentType().startsWith("text/x-gwt-rpc"))
		{
			wrappedRequest = new GWTRequestWrapper(wrappedRequest);
		}
		final CounterServletResponseWrapper wrappedResponse = new CounterServletResponseWrapper(httpResponse);
		final long start = System.currentTimeMillis();
		final long startCpuTime = ThreadInformations.getCurrentThreadCpuTime();
		boolean systemError = false;
		Throwable systemException = null;
		String requestName = getCompleteRequestName(wrappedRequest, false);
		final String completeRequestName = getCompleteRequestName(wrappedRequest, true);
		try
		{
			JdbcWrapper.ACTIVE_THREAD_COUNT.incrementAndGet();
			httpCounter.bindContext(requestName, completeRequestName, httpRequest.getRemoteUser(), startCpuTime);
			httpRequest.setAttribute(CounterError.REQUEST_KEY, completeRequestName);
			CounterError.bindRequest(httpRequest);
			chain.doFilter(wrappedRequest, wrappedResponse);
			wrappedResponse.flushBuffer();
		}
		catch(final Throwable t)
		{
			systemException = t;
			throwException(t);
		}
		finally
		{
			try
			{
				final long duration = Math.max(System.currentTimeMillis() - start, 0);
				final long cpuUsedMillis = (ThreadInformations.getCurrentThreadCpuTime() - startCpuTime) / 1000000;
				JdbcWrapper.ACTIVE_THREAD_COUNT.decrementAndGet();
				putUserInfoInSession(httpRequest);
				if(systemException != null)
				{
					systemError = true;
					final StringWriter stackTrace = new StringWriter(200);
					systemException.printStackTrace(new PrintWriter(stackTrace));
					errorCounter.addRequestForSystemError(systemException.toString(), duration, cpuUsedMillis, stackTrace.toString());
				}
				else
				{
					if(wrappedResponse.getCurrentStatus() >= 400)
					{
						systemError = true;
						errorCounter.addRequestForSystemError("Error" + wrappedResponse.getCurrentStatus(), duration, cpuUsedMillis, null);
					}
				}

				final int responseSize = wrappedResponse.getDataLength();
				if(wrappedResponse.getCurrentStatus() == HttpServletResponse.SC_NOT_FOUND)
				{
					requestName = "Error404";
				}

				httpCounter.addRequest(requestName, duration, cpuUsedMillis, systemError, responseSize);
				log(httpRequest, requestName, duration, systemError, responseSize);
			}
			finally
			{
				httpCounter.unbindContext();
				CounterError.unbindRequest();
			}
		}
	}

	protected final String getMonitoringUrl(HttpServletRequest httpRequest)
	{
		if(monitoringUrl == null)
		{
			final String parameterValue = Parameters.getParameter(Parameter.MONITORING_PATH);
			if(parameterValue == null)
			{
				monitoringUrl = httpRequest.getContextPath() + "/monitoring";
			}
			else
			{
				monitoringUrl = httpRequest.getContextPath() + parameterValue;
			}
		}
		return monitoringUrl;
	}

	private void putUserInfoInSession(HttpServletRequest httpRequest)
	{
		final HttpSession session = httpRequest.getSession(false);
		if(session == null)
		{
			return;
		}
		if(session.getAttribute(SessionInformations.SESSION_COUNTRY_KEY) == null)
		{
			final Locale locale = httpRequest.getLocale();
			if(locale.getCountry().length() > 0)
			{
				session.setAttribute(SessionInformations.SESSION_COUNTRY_KEY, locale.getCountry());
			}
			else
			{
				session.setAttribute(SessionInformations.SESSION_COUNTRY_KEY, locale.getLanguage());
			}
		}
		if(session.getAttribute(SessionInformations.SESSION_REMOTE_ADDR) == null)
		{
			final String forwardedFor = httpRequest.getHeader("X-Forwarded-For");
			final String remoteAddr;
			if(forwardedFor == null)
			{
				remoteAddr = httpRequest.getRemoteAddr();
			}
			else
			{
				remoteAddr = httpRequest.getRemoteAddr() + " forwarded for " + forwardedFor;
			}
			session.setAttribute(SessionInformations.SESSION_REMOTE_ADDR, remoteAddr);
		}
		if(session.getAttribute(SessionInformations.SESSION_REMOTE_USER) == null)
		{
			final String remoteUser = httpRequest.getRemoteUser();
			if(remoteUser != null)
			{
				session.setAttribute(SessionInformations.SESSION_REMOTE_USER, remoteUser);
			}
		}
	}

	private void doMonitoring(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
	{
		if(isRequestNotAllowed(httpRequest))
		{
			httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden access");
			return;
		}
		final Collector collector = filterContext.getCollector();
		final MonitoringController monitoringController = new MonitoringController(collector, null);
		monitoringController.executeActionIfNeeded(httpRequest);
		final JavaInformations javaInformations;
		if(MonitoringController.isJavaInformationsNeeded(httpRequest))
		{
			javaInformations = new JavaInformations(filterConfig.getServletContext(), true);
		}
		else
		{
			javaInformations = null;
		}
		monitoringController.doReport(httpRequest, httpResponse, Collections.singletonList(javaInformations));

		if("stop".equalsIgnoreCase(httpRequest.getParameter(COLLECTOR_PARAMETER)))
		{
			if(filterContext.getTimer() != null)
			{
				filterContext.getTimer().cancel();
			}
			collector.stop();
		}
	}

	private static String getCompleteRequestName(HttpServletRequest httpRequest, boolean includeQueryString)
	{
		String tmp = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
		final int lastIndexOfSemiColon = tmp.lastIndexOf(';');
		if(lastIndexOfSemiColon != -1)
		{
			tmp = tmp.substring(0, lastIndexOfSemiColon);
		}
		final String method;
		if("XMLHttpRequest".equals(httpRequest.getHeader("X-Requested-With")))
		{
			method = "ajax " + httpRequest.getMethod();
		}
		else
		{
			method = httpRequest.getMethod();
		}
		if(!includeQueryString)
		{
			if(httpRequest instanceof GWTRequestWrapper)
			{
				final GWTRequestWrapper wrapper = (GWTRequestWrapper) httpRequest;
				return tmp + '.' + wrapper.getGwtRpcMethodName() + " GWT-RPC";
			}

			return tmp + ' ' + method;
		}
		final String queryString = httpRequest.getQueryString();
		if(queryString == null)
		{
			return tmp + ' ' + method;
		}
		return tmp + '?' + queryString + ' ' + method;
	}

	private boolean isRequestExcluded(HttpServletRequest httpRequest)
	{
		return urlExcludePattern != null && urlExcludePattern.matcher(httpRequest.getRequestURI().substring(httpRequest.getContextPath().length())).matches();
	}

	private boolean isRequestNotAllowed(HttpServletRequest httpRequest)
	{
		return allowedAddrPattern != null && !allowedAddrPattern.matcher(httpRequest.getRemoteAddr()).matches();
	}

	protected void log(HttpServletRequest httpRequest, String requestName, long duration, boolean systemError, int responseSize)
	{
		if(!logEnabled)
		{
			return;
		}
		final String filterName = filterConfig.getFilterName();
		LOG.logHttpRequest(httpRequest, requestName, duration, systemError, responseSize, filterName);
	}

	private static void throwException(Throwable t) throws IOException, ServletException
	{
		if(t instanceof Error)
		{
			throw (Error) t;
		}
		else
		{
			if(t instanceof RuntimeException)
			{
				throw (RuntimeException) t;
			}
			else
			{
				if(t instanceof IOException)
				{
					throw (IOException) t;
				}
				else
				{
					if(t instanceof ServletException)
					{
						throw (ServletException) t;
					}
					else
					{
						throw new ServletException(t.getMessage(), t);
					}
				}
			}
		}
	}

	FilterContext getFilterContext()
	{
		return filterContext;
	}
}
